Skip to content

feat: native examine diagnose & fix - #39

Merged
volen-silo merged 3 commits into
mainfrom
examine-skill
Jun 23, 2026
Merged

feat: native examine diagnose & fix#39
volen-silo merged 3 commits into
mainfrom
examine-skill

Conversation

@volen-silo

@volen-silo volen-silo commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Add the rocm-doctor capability — host examination, diagnosis against a closed catalog of known misconfigurations, and consent-gated fixes — to the rocm binary as native subcommands. The catalog and probe live in the CLI as one source of truth, versioned with the binary and usable standalone (no Python or agent required). The amd/skills rocm-doctor skill then becomes a thin layer that just invokes these commands.

Commands

  • rocm examine [--json] — host probe / report. --json emits the structured Examination document (Linux full parity, Windows best-effort) for tooling. It's a general system inspector, so it always exits 0; the verdict is reported in output and a --json status field (ok / no-amd-gpu / wsl / unsupported-os / degraded). WSL2 is reported with route-out guidance.
  • rocm diagnose [--symptom "…"] [--top N] [--json] — match the host against the 15 closed-catalog failure modes; returns ranked causes with evidence, a fix, a verify step, and upstream routing when nothing matches. Always exits 0; callers read has_match / out_of_scope / route_when_no_match from --json. WSL2 short-circuits as out of scope.
  • rocm fix [<id>] [--yes] [--dry-run] [--device-index N] — apply a known fix (the four safe, auto-applicable ones); risky fixes print their plan and mutate nothing. Run with no id to list fixes. Exit codes: 0 ran · 1 internal error · 2 usage incl. unknown id · 3 not applicable on this host · 4 attempted but failed · 5 declined.

Design

  • The probe, the closed catalog (checks, keyword tables, scoring), and the fix recipes all live in rocm-core (examine / diagnose / fix modules).
  • A field-set test freezes the Examination JSON contract so the probe output and the catalog can't silently diverge.
  • Exit codes report whether a command ran, not what it found — findings are carried in the JSON (status, has_match, out_of_scope). 2 stays reserved for clap usage errors.

Notes / follow-ups

  • WSL2 is intentionally out of scope here — a dedicated follow-up PR will add real WSL2 diagnosis. WSL2 is a distinct platform (it uses /dev/dxg + the Windows host driver, not the amdgpu module or /dev/kfd), so the bare-metal catalog would only produce false positives. This PR detects it and routes out (status: "wsl" / out_of_scope); the follow-up will add a WSL-specific probe + catalog.
  • --framework selection is not yet wired at the CLI (probes default to auto-detect).
  • os_version is the coarse std::env::consts::OS value (not consumed by diagnosis).
  • Windows HIP-SDK probe is an intentional best-effort reduced port.

Base automatically changed from rename-doctor-to-examine to main June 22, 2026 13:54
@juhovainio

juhovainio commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@volen-silo for the sake of clarity and simplicity could we call the commands as such:

  • rocm examine
  • rocm diagnose
  • rocm fix

It seems a bit odd to have diagnose and fix as flags under examine since they serve a different purpose and are separate modules.

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. Full review done. This is a Python→Rust port of the rocm-doctor skill (examine / diagnose / fix), and the core of it is solid: all 15 catalog checks, their scoring/keyword tables, the no-match upstream routing, and the "exactly four auto-applicable fixes" classification match the skill 1:1, the JSON contract is frozen by a test, and CI is green. The issues below are one crash bug, a few behavioral gaps against the skill's documented contract, and some repo hygiene. The first four are blocking.

Blocking

1. Crash: UTF-8 slice panic in probe_env (examine.rs:1091)

format!("{}...[truncated]", &value[..4000])

value.len() > 4000 and &value[..4000] are byte offsets. If PATH or LD_LIBRARY_PATH is over 4000 bytes and byte 4000 lands inside a multibyte character (any accented or non-ASCII path component — realistic on localized systems and CI with deep dependency dirs), Examination::probe() panics and the process aborts (exit 101) instead of returning a clean exit code.

Reproduced on the built binary:

thread 'main' panicked at crates/rocm-core/src/examine.rs:1091:47:
end byte index 4000 is not a char boundary; it is inside 'é' (bytes 3999..4001 of string)

This also breaks the probe() "never fails" contract, and it's a divergence from the script — Python's value[:4000] slices by character and is safe. Suggested fix: truncate on char boundaries, e.g. value.chars().take(4000).collect::<String>(). Please add a regression test with a multibyte value crossing the boundary.

2. diagnose ignores WSL2 and emits false positives

examine detects WSL (Examination::is_wsl) and its exit_code() treats WSL as "this skill can't help here" → exit 2 (examine.rs:269-271). But diagnose never consults is_wsl: run_all_checks (diagnose.rs:1232-1247) filters checkers only by os_family, and WSL2 reports as "linux", so the entire bare-metal Linux catalog runs against a WSL2 box.

On WSL2, ROCm uses /dev/dxg + the Windows host driver, not the in-tree amdgpu module or /dev/kfd, so these misfire:

  • check_5_amdgpu_blacklistedamdgpu_loaded == Some(false) is the normal state on WSL2, not a fault. Fires at score 35.
  • check_4_render_group — render/video groups and /dev/kfd ownership are irrelevant on WSL2. Fires at score 45.
  • check_3_rocm_kernel_unsupported — the amdgpu_loaded == Some(false) branch (diagnose.rs:474-479) adds a spurious DKMS signal.

Net effect: a WSL2 user gets confident diagnoses with remediation (usermod -G render, modprobe amdgpu) that is useless-to-harmful on that platform. Reproduced via rocm diagnose on a WSL2 host (fix-6-path 70, fix-4-render-group 45, fix-5-amdgpu-load 35).

Preferred fix: short-circuit diagnose when e.is_wsl with a route-out message, consistent with exit_code() returning 2; add an is_wsl-true test asserting these no longer fire.

3. examine without --json never applies the exit code

examine() only honors examination.exit_code() inside the --json branch (main.rs:1453-1463); the text path calls render_examine_text() and always returns Ok(()) → exit 0. So on WSL / non-AMD / unsupported-platform hosts, rocm examine --json exits 2 but rocm examine exits 0. Reproduced live on WSL2 (json → 2, no-json → 0). diagnose() already exits correctly in both modes, so this is just an asymmetry in examine. The exit-code logic should apply regardless of output format.

4. examine doesn't replicate the WSL route-out note

examine.py early-returns on WSL with a note pointing at the ROCm-on-WSL install guide and runs no further probes. Examination::probe() (examine.rs:231-264) has no is_wsl branch — it runs the full Linux probe set on WSL and notes comes back empty (verified live: is_wsl: true, notes: []). The exit code is right in --json mode, but the user-facing "you're on WSL, here's where to go" guidance is lost.

Non-blocking (worth addressing or noting in the PR)

  • --framework is unreachable. FrameworkProbe has PyTorch/LlamaCpp/Skip/Auto, but the CLI exposes no flag and both handlers hardcode Auto (main.rs:1454, 1467). The skill documents --framework selection; only auto-detect is reachable.
  • os_version value differs. Script emits platform.platform() (e.g. Linux-6.8.0-…-x86_64-…); the port emits std::env::consts::OS → just "linux"/"windows" (examine.rs:380). Same field name, much thinner value — not consumed by diagnose, so the contract holds, but "field-for-field" only holds for names, not values.
  • Windows HIP-SDK probe is a reduced port (examine.rs:1246-1297): omits the Program Files (x86) scan, the not-loaded hipInfo status, the arch: gfx fallback, version-regex extraction, and GPU-name backfill. Fine if Windows is intentionally "best-effort," but worth stating.
  • check_10 (container) scoring diverges on a null kfd (diagnose.rs:929-932): adds +40 where the script adds 0. Converges on real probe output (the probe always populates kfd), so edge-only — a small guard or comment would close it.
  • Negative --device-index is persisted verbatim (HIP_VISIBLE_DEVICES=-1); a >= 0 check would be cleaner. (No injection risk — it's an i64 argv element.)

Repo hygiene

  • No DCO Signed-off-by on either commit — will fail if DCO is enforced.
  • Branch is behind main — missing the OSS-cleanup commit (#20); please rebase.
  • PR description is stale — it says rocm examine --diagnose / rocm examine --fix, but the final commit split these into separate rocm diagnose / rocm fix subcommands. Please update the description.

What's solid (verified)

  • All 15 checks present; scoring heuristics, keyword tables/weights, score tiers (75/50), OS gating, and UPSTREAM_TRACKERS URLs match 1:1.
  • Auto-fix set is exactly {fix-2, fix-4, fix-6, fix-9}, enforced by a test; runners build Command as argv vectors (no shell interpolation), print before applying, gate on consent, don't self-elevate, and never silently fall back.
  • The frozen top-level-keys test keeps the probe JSON and the catalog from silently diverging.
  • cargo clippy -p rocm-core clean; no internal leaks in the diff.

@volen-silo

volen-silo commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @rominf — thorough review, all four blocking items addressed (plus two of the non-blocking ones). Summary:

Blocking — fixed

  1. UTF-8 panic in probe_env. PATH/LD_LIBRARY_PATH are now truncated on char boundaries (matching Python's value[:4000]) instead of a byte slice, so a multibyte char at the cut no longer aborts probe(). Added a regression test with a multibyte value crossing the 4000 boundary.
  2. diagnose ignored WSL2. Now short-circuits when is_wsl: the bare-metal catalog is not run, so fix-4/fix-5/fix-3/fix-6 no longer misfire. Returns an out-of-scope route message and exits 2, consistent with exit_code(). Added an is_wsl-true test asserting none of those fire (and a non-WSL regression test).
  3. examine text mode ignored the exit code. The out-of-scope exit code now applies in both text and --json (derived from the single ExamineSummary the text path already gathers — no double probe). WSL/non-AMD/unsupported-platform → exit 2 in both formats.
  4. examine lost the WSL route-out note. probe() now early-returns on WSL with the ROCm-on-WSL guidance note and skips the Linux probe set (mirrors examine.py); the text path surfaces the same note.

Non-blocking — fixed

  • check_10 null kfd now contributes 0 (was +40), matching the script.
  • fix-9 rejects a negative --device-index.

Non-blocking — deferred (noted for follow-up)

  • --framework flag still unreachable (handlers hardcode Auto) — can wire it up if you'd like it in this PR.
  • os_version remains the thinner std::env::consts::OS value (field name matches; not consumed by diagnose).
  • Windows HIP-SDK probe stays a best-effort reduced port.

Hygiene

Re: the red windows-build-and-test — that's the pre-existing flaky tui::tests::assistant_support_prompts_from_home_route_to_chat_or_guided_start (its own poll_app_until_idle comment notes it flakes under CPU starvation on the Windows runner). Untouched by this PR and green on Linux; a re-run should clear it.

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 0ba3cbfe. Thanks for the fast turnaround — every change I asked for landed and I verified each one by building the binary and running it on a WSL2 host:

  • ✅ UTF-8 panic in probe_envtruncate_to_chars is char-safe; the input that previously aborted with exit 101 now exits cleanly, with two regression tests.
  • diagnose WSL2 handling — out_of_scope short-circuit + route-out, with tests.
  • examine text-mode exit code + WSL route-out note now present.
  • ✅ fix-10 null-kfd edge corrected; rebased onto current main; PR description updated.

130 tests pass locally. Good work. One new blocking issue, and it traces back to my own previous request — so the fix is a design decision, not just a patch.

Blocking: CI is red on both platforms — the exit-code change broke a pre-existing smoke test

build-and-test and windows-build-and-test both fail with:

smoke failed: rocm examine exited with status 2

scripts/smoke_local.py:261 runs rocm examine, expects exit 0, and asserts the setup inventory (default_engine:, managed_runtimes: 0). CI runners have no AMD GPU, so the new "exit 2 when no AMD GPU" makes that command fail on every GPU-less host.

The real problem this exposes: rocm examine wears two hats. It's both the pre-existing setup inspector you run on any box (GPU or not) and the new host probe where "no AMD GPU → 2" feels right. Encoding a diagnostic verdict in the exit code breaks the inspector role — and it also collides with clap, which already uses exit 2 for usage errors. My earlier "make text mode honor the exit code" request was right about the WSL/JSON asymmetry but, taken literally, made the no-GPU case exit 2 too. That's what the smoke test caught.

Requested change: an exit-code scheme where the code reports execution, not the finding

Findings (no GPU, WSL, no match) belong in the output and --json; the exit code should only say whether the command ran. 2 stays reserved for clap; 1 is the conventional "it failed."

Shared: 0 ran · 1 internal error · 2 usage (clap).

rocm examine (reporter): 0 for any finding — GPU, no GPU, WSL, degraded — surfaced via output and a new --json status field (ok / no-amd-gpu / wsl / unsupported-os / degraded). 1 only if it genuinely can't examine. This is what turns the smoke test green by design rather than by relaxing the test.

rocm diagnose (query): 0 whether it matched, found nothing, or is out of scope — callers read has_match / out_of_scope / route_when_no_match from --json. (Your WSL tests already assert out_of_scope.is_some() + !has_match(), which are unaffected — only the exit-code expectation flips.)

rocm fix (the only command that acts, so the only one with richer states): 0 applied / dry-run / list / print-only plan · 1 internal error · 2 usage incl. unknown fix-id · 3 not applicable on this host (OS mismatch, missing --device-index) — refused, nothing changed · 4 attempted but failed.

Net: agents branch on the JSON (status, out_of_scope, has_match), never on the exit integer — which is strictly richer than the old 2/3 codes. One line in the skill/PR noting that migration covers it; the --json field contract the PR claims is untouched (we add status, remove nothing).

If you'd rather keep rocm diagnose && … working as a shell predicate, the grep model (0 match / 1 no-match / 2 error) is also defensible — but then out-of-scope needs its own code (3) so WSL ≠ no-match, and examine must still stay 0 on the GPU-less box. I'd lean toward the uniform scheme above for consistency.

Minor (non-blocking)

  • DCO sign-off is on only 1 of the 4 commits — the other three lack Signed-off-by; will fail per-commit DCO.
  • --framework is still unreachable at the CLI (enum exists, handlers hardcode Auto). Fine to defer, but worth a note in the PR if it's intentional.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds native rocm examine, rocm diagnose, and rocm fix functionality by porting the rocm-doctor probe/closed-catalog diagnosis/fix runner into Rust, with rocm-core becoming the single source of truth for the catalog and its wire contracts.

Changes:

  • Introduces new rocm-core modules: examine (host probe + Examination JSON), diagnose (15-check closed catalog + report rendering), and fix (consent-gated fix runners + recipe registry).
  • Re-exports the new APIs from rocm-core and adds CLI subcommands in apps/rocm to expose examine/diagnose/fix (including --json and fix options).
  • Adds the regex dependency to support symptom keyword scoring and version parsing helpers in diagnosis.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
crates/rocm-core/src/lib.rs Exposes new examine/diagnose/fix modules and re-exports their public API for the CLI.
crates/rocm-core/src/examine.rs New host probe producing the Examination JSON contract (Linux + Windows best-effort) with contract-freezing tests.
crates/rocm-core/src/diagnose.rs New closed-catalog diagnosis engine with keyword scoring, routing, and text/JSON report output.
crates/rocm-core/src/fix.rs New fix recipe registry plus consent-gated runners for auto-applicable fixes and listing/printing plans.
crates/rocm-core/Cargo.toml Adds regex dependency required by diagnosis logic.
Cargo.lock Locks regex into the workspace dependency graph.
apps/rocm/src/main.rs Adds examine --json, diagnose, and fix subcommands and dispatch logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +741 to +745
if let Err(exc) = append_line(
&rc_file,
"# Added by rocm examine (fix-6-path)",
&export_line,
) {
Comment on lines +850 to +854
if let Err(exc) = append_line(
&rc_file,
"# Added by rocm examine (fix-9-igpu-dgpu)",
&export_line,
) {
Comment on lines +925 to +948
fn newest_rocm_install_dir() -> String {
for root in [
r"C:\Program Files\AMD\ROCm",
r"C:\Program Files (x86)\AMD\ROCm",
] {
if let Ok(entries) = std::fs::read_dir(root) {
let mut versions: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| {
p.is_dir()
&& p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.chars().next().is_some_and(|c| c.is_ascii_digit()))
})
.collect();
versions.sort();
if let Some(latest) = versions.last() {
return latest.to_string_lossy().into_owned();
}
}
}
String::new()
}
Comment thread apps/rocm/src/main.rs
Comment on lines +1453 to +1457
// `rocm examine` is the general system inspector: the exit code reports
// whether it RAN, not what it found. Any finding (no GPU, WSL, degraded) is
// surfaced in the output and the `--json` `status` field, and the command
// exits 0; a genuine inability to examine propagates as an error via `?`.
if json {
Comment on lines +1282 to +1290
let mut versions: Vec<String> = entries
.flatten()
.filter(|entry| entry.path().is_dir())
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
versions.sort();
if let Some(latest) = versions.last() {
root = base.join(latest).to_string_lossy().into_owned();
}
Comment on lines +780 to +786
let new_path = if user_path.is_empty() {
bin_dir.clone()
} else {
format!("{user_path};{bin_dir}")
};
println!("Plan: prepend {bin_dir} to your User PATH:");
println!(" setx PATH \"{new_path}\"");
Add the rocm-doctor capability to the rocm binary: probe the host,
diagnose against a closed catalog of known ROCm/PyTorch/llama.cpp
misconfigurations, and apply consent-gated fixes. The probe, the closed
catalog (checks, keyword tables, scoring), and the fix recipes live in
rocm-core as one source of truth, versioned with the binary and usable
standalone -- no external scripts or agent required.

- examine: structured Examination host probe (Linux full parity, Windows
  best-effort), with a machine-readable JSON form for tooling
- diagnose: the 15 closed-catalog checks with evidence, a fix, a verify
  step, and upstream routing when nothing matches
- fix: consent-gated runners for the four safe fixes; risky fixes print
  their plan and mutate nothing

A field-set test freezes the Examination JSON contract so the probe output
and the catalog cannot silently diverge.

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
Address review feedback: diagnose and fix are distinct verbs backed by
separate modules, so expose them as top-level commands rather than mode
flags under `examine`.

- rocm examine [--json]        host probe / report
- rocm diagnose [--symptom][--top][--json]  match the closed catalog
- rocm fix [<id>][--yes][--dry-run][--device-index]  apply or list fixes

Also register diagnose/fix in the natural-language allowlist so they
dispatch as structured commands instead of falling through to the
freeform planner. Help strings updated to the new command names.

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
…heme

- probe_env: char-boundary truncation for PATH/LD_LIBRARY_PATH so a
  multibyte char at the truncation cut no longer panics probe(); the cap is
  a named constant (16k chars) chosen well past any realistic ROCm bin entry
  so the fix-6 PATH check isn't tripped by truncation. Regression test added.
- Exit codes report execution, not findings (per review):
  - examine: always exits 0 (a genuine inability to examine propagates as
    an error). The verdict is a --json `status` field
    (ok / no-amd-gpu / wsl / unsupported-os / degraded). WSL2 also skips the
    Linux probe set and shows a route-out note.
  - diagnose: always exits 0; callers read has_match / out_of_scope /
    route_when_no_match from --json. WSL2 short-circuits with out_of_scope.
  - fix: 0 ok/dry-run/list/print, 1 internal error, 2 usage incl. unknown
    fix-id, 3 not applicable (OS mismatch / missing or negative
    --device-index), 4 attempted-but-failed, 5 user declined.
- check-10 (container): a null kfd contributes 0.

Adds is_wsl tests for diagnose, status-precedence and multibyte-truncation
tests for examine.

Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
@volen-silo
volen-silo added this pull request to the merge queue Jun 23, 2026
Merged via the queue into main with commit 6d679ff Jun 23, 2026
6 checks passed
@volen-silo
volen-silo deleted the examine-skill branch June 23, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants